<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go 应用代码解析 - OpenTelemetry 初始化</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>2. OpenTelemetry (OTel) 初始化</h1>
        <nav>
            <a href="index.html">概述</a> |
            <a href="01_setup_dependencies.html">1. 依赖与全局变量</a> |
            <a href="02_opentelemetry_init.html" class="active">2. OpenTelemetry 初始化</a> |
            <a href="03_database_redis_init.html">3. 数据库与 Redis 初始化</a> |
            <a href="04_web_server_routing.html">4. Web 服务器与路由</a> |
            <a href="05_request_handlers.html">5. HTTP 请求处理</a> |
            <a href="06_business_logic.html">6. 核心业务逻辑</a> |
            <a href="07_main_startup_shutdown.html">7. 程序启动与关闭</a>
        </nav>
    </header>

    <main>
        <section id="otel-intro">
            <h2>什么是 OpenTelemetry (OTel)？为什么要初始化它？</h2>
            <p>想象一下，你的程序处理一个用户请求，这个请求可能会经过好几个步骤：接收请求、查询缓存、查询数据库、处理数据、返回响应。如果程序变慢或者出错，你想知道是哪个环节出了问题，或者每个环节花了多少时间。</p>
            <p><strong>OpenTelemetry (简称 OTel)</strong> 就是这样一个行业标准和工具集，它可以帮助你：</p>
            <ul>
                <li><strong>追踪 (Tracing):</strong> 记录一个请求的完整路径，以及在每个步骤（称为 Span）花费的时间和关键信息。就像给请求安插了一个侦探，全程跟踪记录。</li>
                <li><strong>指标 (Metrics):</strong> 收集服务的量化数据，比如请求速率、错误率、CPU 使用率等。</li>
                <li><strong>日志 (Logs):</strong> 关联日志信息到特定的追踪上，方便排查问题。</li>
            </ul>
            <p>在这个程序里，我们主要使用了 OTel 的 <strong>追踪</strong> 功能。在程序启动时初始化 OTel，就像是给这个“侦探系统”设定好工作规则、配备好工具，让它能在程序运行时有效地收集信息。</p>
             <p>我们来看看 `initTracerProvider` 函数是怎么做这个初始化工作的。</p>

            <h3>OTel 核心概念图</h3>
            <div class="diagram-container">
                <object type="image/svg+xml" data="diagrams/otel_components.svg" class="diagram">
                    你的浏览器不支持 SVG，无法显示 OTel 组件图。
                </object>
            </div>
             <p><strong>图解核心概念：</strong></p>
             <ul>
                 <li><strong>Trace (追踪):</strong> 代表一个完整的请求流程，由多个 Span 组成。</li>
                 <li><strong>Span (跨度/单元):</strong> 代表追踪中的一个具体操作步骤（如一次 HTTP 请求处理、一次数据库查询）。Span 有开始时间、结束时间、名称、属性（键值对信息）、事件（某个时间点发生的事）和状态（成功/失败）。</li>
                 <li><strong>Tracer (追踪器):</strong> 用来创建 Span 的工具，由 TracerProvider 提供。</li>
                 <li><strong>TracerProvider (追踪器提供者):</strong> 管理 Tracer 的工厂，配置 Span 的处理方式（如采样、导出）。<strong>这是 OTel SDK 的核心。</strong></li>
                 <li><strong>Exporter (导出器):</strong> 负责将收集到的 Span 数据发送到外部系统（如 Jaeger、Zipkin、或像本例一样打印到控制台）。</li>
                 <li><strong>Resource (资源):</strong> 描述产生遥测数据（Span、Metric、Log）的实体，通常是你的服务本身（服务名、版本号、环境等）。</li>
                 <li><strong>Propagator (传播器):</strong> 定义如何在服务之间（比如通过 HTTP 头）传递追踪上下文（Trace ID），以实现分布式追踪。</li>
                 <li><strong>Context (上下文):</strong> Go 中的 `context.Context` 用于在函数调用链中隐式地传递数据，OTel 用它来传递当前的 Trace 和 Span 信息。</li>
             </ul>

        </section>

        <section id="init-tracer-provider">
            <h2>`initTracerProvider` 函数详解</h2>
            <pre><code class="language-go">// initTracerProvider 初始化 OpenTelemetry SDK TracerProvider。
func initTracerProvider() (func(context.Context) error, error) {
	log.Println("初始化 OTel Tracer Provider...") // 打印日志，表示开始初始化

	// 1. 创建 Exporter (导出器): 决定追踪数据去哪里
	// 这里用 stdouttrace 把追踪数据“漂亮地”打印到控制台
	exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
	if err != nil {
		// 如果创建失败，返回错误
		return nil, fmt.Errorf("创建 stdout exporter 失败: %w", err)
	}
	log.Println("OTel Exporter (stdout) 创建成功。")

	// 2. 创建 Resource (资源): 描述我们的服务信息
	// resource.Default() 提供一些默认信息 (如 Go 版本) 和 Schema URL
	// resource.NewSchemaless(...) 提供我们自定义的信息
	// resource.Merge() 把它们合并起来
	res, err := resource.Merge(
		resource.Default(), // 包含 Go SDK 的基本信息和 Schema URL
		resource.NewSchemaless(
			// semconv 提供标准化的属性名
			semconv.ServiceName("gin-gorm-mysql-redis-service"), // 服务名，非常重要！
			semconv.ServiceVersion("v1.4.3"),                    // 服务版本
			attribute.String("environment", "development"),      // 自定义属性：运行环境
		),
	)
	if err != nil {
		// 合并一般不会失败，但还是检查一下
		return nil, fmt.Errorf("创建 resource 失败: %w", err)
	}
	log.Println("OTel Resource 创建成功，服务名:", "gin-gorm-mysql-redis-service")

	// 3. 创建 TracerProvider (追踪器提供者): 这是 OTel 的核心配置
	tp := sdktrace.NewTracerProvider(
		// WithSampler: 决定哪些请求需要被追踪 (AlwaysSample 表示全部追踪)
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		// WithBatcher: 将收集到的 Span 批量发送给 Exporter，提高效率
		sdktrace.WithBatcher(exporter),
		// WithResource: 关联上面创建的服务信息 Resource
		sdktrace.WithResource(res),
	)
	log.Println("OTel TracerProvider 配置完成。")

	// 4. 设置全局 Provider: 让 otel.GetTracerProvider() 能拿到我们刚创建的 tp
	otel.SetTracerProvider(tp)

	// 5. 设置全局 Propagator (传播器): 定义如何在请求间传递追踪信息
	// TraceContext: W3C 标准的追踪上下文传播协议 (traceparent, tracestate 头)
	// Baggage: 允许在请求链中传递一些自定义的键值对信息
	// NewCompositeTextMapPropagator: 组合使用多种传播协议
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))
	log.Println("OTel 全局 Provider 和 Propagator 设置完成。")

	// 返回一个函数，用于在程序关闭时优雅地停止 TracerProvider
	return tp.Shutdown, nil
}</code></pre>
            <p><strong>步骤拆解：</strong></p>
            <ol>
                <li><strong>创建 Exporter:</strong> 我们选择了 `stdouttrace`，意思是把收集到的追踪信息直接打印在程序运行的控制台（命令行界面）上。这对于本地开发调试很方便。在生产环境中，通常会选择其他的 Exporter，把数据发送到专门的追踪系统，如 Jaeger 或 Zipkin。</li>
                <li><strong>创建 Resource:</strong> 我们告诉 OTel，现在运行的这个程序叫什么名字 (`gin-gorm-mysql-redis-service`)、版本号是多少、运行在什么环境 (`development`)。这些信息会附加到所有从此服务发出的追踪数据上，方便在追踪系统中区分来源。</li>
                <li><strong>创建 TracerProvider:</strong> 这是配置 OTel SDK 的核心。
                    <ul>
                        <li><code>WithSampler(sdktrace.AlwaysSample())</code>: 设置采样策略。`AlwaysSample` 表示对每一个请求都进行完整的追踪。在高流量的生产环境中，可能会选择概率采样（比如只追踪 10% 的请求）以减少性能开销。</li>
                        <li><code>WithBatcher(exporter)</code>: 设置 Span 处理器。`WithBatcher` 表示先把收集到的 Span 放在内存里攒一批，然后一次性发给 Exporter，而不是来一个发一个，这样更高效。</li>
                        <li><code>WithResource(res)</code>: 把刚才创建的服务信息（Resource）关联到这个 Provider 上。</li>
                    </ul>
                    这样，我们就得到了一个配置好的 `TracerProvider` 实例 `tp`。
                </li>
                <li><strong>设置全局 Provider:</strong> 通过 `otel.SetTracerProvider(tp)`，我们将刚刚创建的 `tp` 注册为全局默认的 TracerProvider。这样，程序中其他地方可以通过 `otel.Tracer("my-tracer-name")` 来获取一个基于这个配置的 Tracer 实例。</li>
                <li><strong>设置全局 Propagator:</strong> 通过 `otel.SetTextMapPropagator(...)`，我们告诉 OTel 如何在不同的服务之间传递追踪信息（主要是 Trace ID 和 Span ID）。这里同时启用了 `TraceContext` (W3C 标准) 和 `Baggage` (传递额外业务数据) 两种协议。当我们的服务收到一个带有特定 HTTP 头（如 `traceparent`）的请求时，或者当我们的服务调用另一个也集成了 OTel 的服务时，Propagator 就负责读取或写入这些头信息，把分散在不同服务中的 Span 串联成一个完整的 Trace。</li>
                <li><strong>返回 Shutdown 函数:</strong> `TracerProvider` 在完成工作后需要被优雅地关闭（比如确保所有缓冲区的 Span 都被导出）。这个函数返回一个 `tp.Shutdown` 函数，我们会在程序退出前调用它。</li>
            </ol>
            <p>初始化完成后，全局的 OTel 追踪器 (`tracer = otel.Tracer(...)` 在 `main` 函数中获取) 就可以使用了。程序中的其他 OTel 集成（如 Gin、GORM、Redis 的插件）也会自动使用这里配置好的全局 Provider。</p>
        </section>

        <section id="navigation">
            <h2>下一步</h2>
            <p>配置好了“侦探系统” OTel，接下来我们需要连接它要监控的目标：数据库和缓存。</p>
            <p>➡️ <a href="03_database_redis_init.html">点击这里，学习数据库 (MySQL/GORM) 和 Redis 的初始化</a></p>
        </section>
    </main>

    <footer>
        <p>Go 应用代码解析</p>
    </footer>
</body>
</html>